﻿using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Client;



namespace Crm2011.Framework.Core
{
    /// <summary>
    /// ScriptEase uses the concept of a connection string to connect to the Microsoft Dynamics CRM server. 
    /// This is similar to the concept of connection strings used with Microsoft SQL Server. 
    /// </summary>
    /// <example>
    /// How to create an <see cref="IOrganizationService"/> from a connection string:
    /// <code>
    /// Microsoft.Xrm.Client.CrmConnection crmConnection = Microsoft.Xrm.Client.CrmConnection.Parse( crmConnectionString.GetConnectionStringClearText());
    /// Microsoft.Xrm.Sdk.IOrganizationService orgService = new Microsoft.Xrm.Client.Services.OrganizationService(crmConnection);
    /// using (var context = new ServiceContext(orgService))
    ///     { /* ... */ }
    /// </code>
    /// </example>
    /// <seealso cref="http://msdn.microsoft.com/en-us/library/ff681567.aspx"/>
    public class CrmConnectionString
    {
        private readonly Dictionary<string, string> _tokens = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        private readonly Dictionary<string, string> _tokenTranslations
            = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
                {
                    {"Url", "Server"}, {"Service Uri", "Server"}, {"ServiceUri", "Server"},
                    {"Username", "User ID"}, {"UserId", "User ID"}, 
                    {"DiscoveryUrl", "Discovery URL"}
                };



        public CrmConnectionString()
            : this(null)
        {}



        /// <summary>
        /// Initializes a new instance of the <see cref="CrmConnectionString"/> class.
        /// </summary>
        /// <remarks>
        /// After initialization the connection string is 'tokenized' and you can access 
        /// all the tokens and their values through the <see cref="Tokens"/> property
        /// </remarks>
        /// <param name="cns">The connection string.</param>
        public CrmConnectionString(string cns)
        {
            if (string.IsNullOrEmpty(cns)) return;

            string[] connectionStringElements = cns.Split(';');
            for (int i = 0; i < connectionStringElements.Length; i++)
            {
                string element = connectionStringElements[i].Trim();
                if (String.IsNullOrEmpty(element)) continue;

                int indexOfEqual = element.IndexOf('=');
                if (indexOfEqual < 0) throw new InvalidOperationException("Invalid connection string!");

                // use token translation to 'normalize' token names
                string key = element.Substring(0, indexOfEqual);
                if (_tokenTranslations.ContainsKey(key)) key = _tokenTranslations[key];

                string value = element.Substring(indexOfEqual + 1, element.Length - indexOfEqual - 1);
                _tokens[key] = string.IsNullOrEmpty(value) ? null : value;
            }

            // if Password element was set, determine if it was a secure or clear-text password
            if (this.Password != null && this.Password.Length > 100)
                this.Password = decryptString(this.Password);
        }



        public Dictionary<string, string> Tokens
        {
            get { return _tokens; }
        }



        /// <summary>
        /// Gets the connection string that is build 
        /// from the current <see cref="Tokens"/>. 
        /// </summary>
        public string GetConnectionStringClearText()
        {
            return getConnectionString(secure: false);
        }



        /// <summary>
        /// Gets the connection string that is build 
        /// from the current <see cref="Tokens"/>. 
        /// </summary>
        public string GetConnectionStringSecure()
        {
            return getConnectionString(secure: true);
        }



        private string getConnectionString(bool secure)
        {
            StringBuilder result = new StringBuilder(128);
            foreach (KeyValuePair<string, string> tokenKeyValue in _tokens)
            {
                if (this.DefaultCredentials && (tokenKeyValue.Key == "User ID" ||
                                                tokenKeyValue.Key == "Password" ||
                                                tokenKeyValue.Key == "Domain")) continue;

                result = result.AppendFormat("{0}={1};", tokenKeyValue.Key,
                                             (secure && tokenKeyValue.Key == "Password") ? encryptString(tokenKeyValue.Value) : tokenKeyValue.Value);
            }

            return result.ToString();
        }


        #region Default Tokens


        public AuthenticationProviderType AuthenticationType
        {
               
            set { _tokens["Authentication"] = value.ToString(); }
            get
            {
                if (!_tokens.ContainsKey("Authentication"))
                {
                    // try to guess Authentication
                    if(this.Server.Contains("dynamics.com")) this.AuthenticationType = AuthenticationProviderType.LiveId;
                    else if (!String.IsNullOrEmpty(this.Domain)) this.AuthenticationType = AuthenticationProviderType.ActiveDirectory;
                    else if (String.IsNullOrEmpty(this.Password)) this.AuthenticationType = AuthenticationProviderType.ActiveDirectory;
                    else if (this.DiscoveryUrl.ToLower().Contains(".local")) this.AuthenticationType = AuthenticationProviderType.ActiveDirectory;
                    else this.AuthenticationType = AuthenticationProviderType.LiveId;
                }

                string provider = _tokens["Authentication"].ToLower();

                if (provider == AuthenticationProviderType.ActiveDirectory.ToString().ToLower()) return AuthenticationProviderType.ActiveDirectory;
                if (provider == AuthenticationProviderType.LiveId.ToString().ToLower()) return AuthenticationProviderType.LiveId;

                throw new ApplicationException("Invalid Authentication setting! Allowed values: ActiveDirectory, LiveId");
            }
        }


        /// <summary>
        /// Specifies the URL of the Microsoft Dynamics CRM server. 
        /// It can be http or https. The port is optional if it is http over port 80 or https over port 443. 
        /// The server URL is typically in this form: <c>http://crm-server:port/organization-name</c>.
        /// The organization-name is required.<br/>
        /// Recognized Tokens: Url=; Server=;<br/>
        /// Conenction String Token: Server=
        /// </summary>
        public string Server
        {
            get { return _tokens.ContainsKey("Server") ? _tokens["Server"] : null; }
            set { _tokens["Server"] = value; }
        }


        public string DiscoveryUrl
        {
            get { return _tokens.ContainsKey("Discovery Url") && !string.IsNullOrEmpty(_tokens["Discovery Url"]) ? _tokens["Discovery Url"] : null; }
            set { _tokens["Discovery Url"] = value; }
        }


        public string OrganizationUrl
        {
            get { return _tokens.ContainsKey("Organization Url") && !string.IsNullOrEmpty(_tokens["Organization Url"]) ? _tokens["Organization Url"] : null; }
            set { _tokens["Organization Url"] = value; }
        }


        public bool DefaultCredentials
        {
            get { return (this.Password == null); }
        }


        /// <summary>
        /// Specifies the user name of the service account that is used to connect using AD, SPLA, 
        /// or Passport authentication types. It is not needed for integrated authentication. 
        /// The format for this is specific to the authentication type: domain\username for AD and 
        /// SPLA authentication types and a Windows Live ID for Passport authentication.
        /// Recognized Tokens: Username=; User ID=;<br/>
        /// Conenction String Token: User ID=
        /// </summary>
        public string UserId
        {
            get { return _tokens.ContainsKey("User ID") ? _tokens["User ID"] : null; }
            set { _tokens["User ID"] = value; }
        }


        /// <summary>
        /// Specifies the password of the service account that is used to connect using AD, SPLA, or Passport authentication as clear text.
        /// </summary>
        public string Password
        {
            get { return _tokens.ContainsKey("Password") && !string.IsNullOrEmpty(_tokens["Password"]) ? _tokens["Password"] : null; }
            set { _tokens["Password"] = value; }
        }


        /// <summary>
        /// Gets or sets the Domain.
        /// </summary>
        public string Domain
        {
            get { return _tokens.ContainsKey("Domain") ? _tokens["Domain"] : null; }
            set { _tokens["Domain"] = value; }
        }





        public string DeviceId
        {
            get { return _tokens.ContainsKey("Device ID") ? _tokens["Device ID"] : null; }
            set { _tokens["Device ID"] = value; }
        }


        public string DevicePassword
        {
            get { return _tokens.ContainsKey("Device Password") ? _tokens["Device Password"] : null; }
            set { _tokens["Device Password"] = value; }
        }


        public string OrganizationName
        {
            get { return _tokens.ContainsKey("OrganizationName") ? _tokens["OrganizationName"] : null; }
            set { _tokens["OrganizationName"] = value; }
        }


        #endregion


        #region Encryption


        private static string encryptString(string value)
        {
            if (string.IsNullOrEmpty(value)) return value;

            byte[] encryptedBytes = ProtectedData.Protect(Encoding.UTF8.GetBytes(value), null, DataProtectionScope.CurrentUser);
            return Convert.ToBase64String(encryptedBytes);
        }



        private static string decryptString(string value)
        {
            if (string.IsNullOrEmpty(value)) return value;

            byte[] decryptedBytes = ProtectedData.Unprotect(Convert.FromBase64String(value), null, DataProtectionScope.CurrentUser);
            return 0 == decryptedBytes.Length ? null : Encoding.UTF8.GetString(decryptedBytes, 0, decryptedBytes.Length);
        }


        #endregion
    }
}